package buxter.api.httpxml;

import java.math.BigDecimal;

import javax.xml.bind.JAXBElement;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
import org.springframework.xml.transform.StringResult;
import org.springframework.xml.transform.StringSource;

import buxter.api.commons.ApiException;
import buxter.api.commons.signature.RequestSignatureHandlerSHA1;
import buxter.api.schema.CancelTransactionRequest;
import buxter.api.schema.CancelTransactionResponse;
import buxter.api.schema.CreateTransactionRequest;
import buxter.api.schema.CreateTransactionResponse;
import buxter.api.schema.ErrorResponse;
import buxter.api.schema.GetTransactionStatusRequest;
import buxter.api.schema.GetTransactionStatusResponse;
import buxter.api.schema.Money;
import buxter.api.schema.ObjectFactory;

/**
 * This is a client implementation for buxter API 
 * using simple CGI based Buxter endpoint 
 * 
 * @author thijs.reus
 * @author andrei.martchouk
 * @author matthias.gall
 * @author natasha.martchouk
 *
 */
public class BuxterApiHttpXmlClient {
	private static final Logger logger = Logger.getLogger(BuxterApiHttpXmlClient.class);	
	private ObjectFactory jaxbFactory;
	private Marshaller marshaller;
	private Unmarshaller unmarshaller;
	private HttpClient httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
	
	/** signatureHandler from the module buxter-api.commons */
	private RequestSignatureHandlerSHA1 signatureHandler;
	
	/** The API configuration Id */
	private long apiConfigurationID;
	
	/** The end point of the Buxter service */
	private String httpSeviceEndPoint;
	
	public CreateTransactionResponse createTransaction(
			final BigDecimal amount, final String currency, 
			final String description, final String title, final long buyerFbUserID, 
			final String returnUrl, final String externalId) throws ApiException{
		CreateTransactionRequest request = new CreateTransactionRequest();
		CreateTransactionResponse tres = null;
		
		request.setAmount(createMoney(amount, currency));
		request.setDescription(description);
		request.setBuyerFbUserID(buyerFbUserID);
		request.setReturnURL(returnUrl);
		request.setExternalID(externalId);
		request.setTitle(title);
		request.setApiConfigurationID(apiConfigurationID);
				
		request.setRequesterSignature(
				getCreateTransactionSignature(
						request.getApiConfigurationID(), 
						request.getBuyerFbUserID(), 
						request.getAmount().getAmount(), 
						request.getAmount().getCurrency()));
		
		try {
			logger.debug("*** Calling HTTP createTransaction");
			final JAXBElement<?> response = invokeHTTP(jaxbFactory.createCreateTransactionRequest(request));
			try{
				tres = (CreateTransactionResponse)response.getValue();
				if(logger.isDebugEnabled() && null != tres){
					logger.debug("*** got CreateTransactionResponse: " + tres.getTransactionID() +":"+tres.getTransactionStatus());
				}
			}
			catch(ClassCastException cce){
				errorToException(response); 
			}
		} 
		catch (Exception e) {
			logger.error("*** Could not invoke HTTP api: " + e);
			throw new ApiException(e);
		}
		
		return tres;
	}
	
	/**
	 * GetTransactionStatus request
	 */
	public GetTransactionStatusResponse getTransaction(final long txId) throws ApiException {
		final GetTransactionStatusRequest request = new GetTransactionStatusRequest();
		GetTransactionStatusResponse sres = null;
		request.setTransactionID(txId);
		request.setApiConfigurationID(apiConfigurationID);
		request.setRequesterSignature(getGetTransactionStatusSignature(apiConfigurationID, txId));

		try {
			logger.debug("Calling HTTP getTransactionStatus");
			final JAXBElement<?> response = invokeHTTP(jaxbFactory.createGetTransactionStatusRequest(request));
			try{
				sres = (GetTransactionStatusResponse)response.getValue();
			}
			catch(ClassCastException cce){
				errorToException(response); 
			}
			
			if(logger.isDebugEnabled() && null != sres){
				logger.debug("*** HTTP getTransactionStatus result: " + sres.getTransactionID() +" : "+ sres.getTransactionStatus());
			}
		} 
		catch (Exception e) {
			logger.error("Could not invoke HTTP api: " + e);
			throw new ApiException(e);
		}
		
		return sres;
	}
	
	/**
	 * GetTransactionStatus request
	 */
	public CancelTransactionResponse cancelTransaction(final long txId) 
		throws ApiException {
		final CancelTransactionRequest request = new CancelTransactionRequest();
		CancelTransactionResponse cresp = null;
		request.setTransactionID(txId);
		request.setApiConfigurationID(apiConfigurationID);
		request.setRequesterSignature(getCancelTransactionSignature(apiConfigurationID, txId));

		try {
			logger.debug("Calling HTTP getTransactionStatus");
			final JAXBElement<?> response = invokeHTTP(jaxbFactory.createCancelTransactionRequest(request));
			try{
				cresp = (CancelTransactionResponse)response.getValue();
			}
			catch(ClassCastException cce){
				errorToException(response); 
			}
			
			if(logger.isDebugEnabled() && null != cresp){
				logger.debug("*** HTTP getTransactionStatus result: " + cresp.getTransactionID() +" : "+ cresp.getTransactionStatus());
			}
		} 
		catch (Exception e) {
			logger.error("Could not invoke HTTP api: " + e);
			throw new ApiException(e);
		}
		
		return cresp;
	}

	private void errorToException(final JAXBElement<?> response)
			throws ApiException {
		logger.debug("*** Setting ErrorResponse");
		ErrorResponse er = (ErrorResponse)response.getValue();
		throw new ApiException(er.getName().name(), er.getDetail().name(), er.getDescription());
	}
	
	private String getCreateTransactionSignature(final long apiConfigurationID, final long receiverUserID, final String amount, final String currency) throws ApiException {
		final String parameterValues = signatureHandler.concatenate(receiverUserID, amount, currency);
		final String requestName = jaxbFactory.createCreateTransactionRequest(null).getName().getLocalPart();
		return signatureHandler.createSignature(requestName, parameterValues);
	}
	
	private String getGetTransactionStatusSignature(final long apiConfigurationID, final long transactionID) throws ApiException {
		final String parameterValues = signatureHandler.concatenate(transactionID);
		final String requestName = jaxbFactory.createGetTransactionStatusRequest(null).getName().getLocalPart();
		return signatureHandler.createSignature(requestName, parameterValues);
	}
	
	private String getCancelTransactionSignature(final long apiConfigurationID, final long transactionID) throws ApiException {
        final String parameterValues = signatureHandler.concatenate(transactionID);
        final String requestName = jaxbFactory.createCancelTransactionRequest(null).getName().getLocalPart();
        return signatureHandler.createSignature(requestName, parameterValues);
}

	/**
	 * Composes the HTTP request by marshalling the request, executes
	 * the HTTP request, and unmarshalls the result.
	 * 
	 * @param request
	 * @return
	 * @throws Exception
	 */
	private JAXBElement<?> invokeHTTP(JAXBElement<?> request) throws Exception {
		final PostMethod post = new PostMethod(getHttpSeviceEndPoint());
		post.setParameter("q", marshall(request));
		httpClient.executeMethod(post);
		return unmarshall(post.getResponseBodyAsString());
	}
	
	/**
	 * Creates a {@link Money} instance with the given amount and currency.
	 * 
	 * @param amount
	 * @param currency
	 * @return
	 */
	private Money createMoney(final BigDecimal amount, final String currency) {
		final Money result = new Money();
		result.setAmount(amount.toString());
		result.setCurrency(currency);
		return result;
	}
	
	/**
	 * Unmarshalls the given input into an XML object tree.
	 *  
	 * @param input
	 * @return
	 */
	private JAXBElement<?> unmarshall(final String input) throws Exception {
		return (JAXBElement<?>)unmarshaller.unmarshal(new StringSource(input));
	}
	
	/**
	 * Marshalls the given JAXBElement into an XML string.
	 * 
	 * @param input
	 * @return
	 * @throws Exception
	 */
	private String marshall(final JAXBElement<?> input) throws Exception {
		final StringResult result = new StringResult();
		marshaller.marshal(input, result);
		return result.toString();
	}



	public HttpClient getHttpClient() {
		return httpClient;
	}

	@Required
	public void setHttpClient(HttpClient httpClient) {
		this.httpClient = httpClient;
	}

	public ObjectFactory getJaxbFactory() {
		return jaxbFactory;
	}

	@Required
	public void setJaxbFactory(ObjectFactory jaxbFactory){
		this.jaxbFactory = jaxbFactory;
	}
	
	public String getHttpSeviceEndPoint() {
		return httpSeviceEndPoint;
	}
	
	@Required
	public void setHttpSeviceEndPoint(String httpSeviceEndPoint){
		this.httpSeviceEndPoint = httpSeviceEndPoint;
	}
	
	public RequestSignatureHandlerSHA1 getSignatureHandler() {
		return signatureHandler;
	}

	@Required
	public void setSignatureHandler(RequestSignatureHandlerSHA1 signatureHandler) {
		this.signatureHandler = signatureHandler;
	}
	
	public Marshaller getMarshaller() {
		return marshaller;
	}

	@Required
	public void setMarshaller(Marshaller marshaller) {
		this.marshaller = marshaller;
	}

	public Unmarshaller getUnmarshaller() {
		return unmarshaller;
	}

	@Required
	public void setUnmarshaller(Unmarshaller unmarshaller) {
		this.unmarshaller = unmarshaller;
	}

	public long getApiConfigurationID() {
		return apiConfigurationID;
	}

	@Required
	public void setApiConfigurationID(long apiConfigurationID) {
		this.apiConfigurationID = apiConfigurationID;
	}


}
